package com.lzy.mall.product.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.lzy.mall.common.constant.Constant;
import com.lzy.mall.product.entity.PmsCategory;
import com.lzy.mall.product.dao.PmsCategoryDao;
import com.lzy.mall.product.service.PmsCategoryBrandRelationService;
import com.lzy.mall.product.service.PmsCategoryService;
import com.lzy.mall.product.vo.Catalog3Vo;
import com.lzy.mall.product.vo.Catelog2Vo;
import com.lzy.mall.product.vo.PmsCategoryVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


/**
 * 商品三级分类(PmsCategory)表服务实现类
 *
 * @author lzy
 * @since 2021-08-25 11:41:12
 */
@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
@Slf4j
public class PmsCategoryServiceImpl implements PmsCategoryService {
    private final PmsCategoryDao pmsCategoryDao;

    private final PmsCategoryBrandRelationService pmsCategoryBrandRelationService;

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    @Override
    public List<PmsCategoryVo> getTree3() {
        List<PmsCategoryVo> list = pmsCategoryDao.getTree();
        List<PmsCategoryVo> level1 = CollUtil.newArrayList();
        List<PmsCategoryVo> level2 = CollUtil.newArrayList();
        List<PmsCategoryVo> level3 = CollUtil.newArrayList();
        list.forEach(vo -> {
            if (Constant.CAT_LEVEL1.equals(vo.getCatLevel())){
                level1.add(vo);
            }
            if (Constant.CAT_LEVEL2.equals(vo.getCatLevel())){
                level2.add(vo);
            }
            if (Constant.CAT_LEVEL3.equals(vo.getCatLevel())){
                level3.add(vo);
            }
        });
        level2.forEach(vo -> {
            List<PmsCategoryVo> collect = level3.stream().filter(a -> vo.getCatId().equals(a.getParentCid())).collect(Collectors.toList());
            vo.setChildren(collect);
        });
        level1.forEach(vo -> {
            List<PmsCategoryVo> collect = level2.stream().filter(a -> vo.getCatId().equals(a.getParentCid())).collect(Collectors.toList());
            vo.setChildren(collect);
        });

        return level1;
    }

    @Override
    public void removeMenuByIds(Long[] ids) {
        //TODO  参数引用检查
        ArrayList<Long> list = CollUtil.toList(ids);
        List<PmsCategory> select = pmsCategoryDao.createLambdaQuery()
                .andIn(PmsCategory::getCatId, list)
                .select();
        select.forEach(pmsCategory -> {
            pmsCategory.setShowStatus(Constant.CAT_NOT_SHOW_STATUS);
        });
        pmsCategoryDao.getSQLManager()
                .updateBatchTemplateById(PmsCategory.class,select);
    }

    @Override
    public void saveMenu(PmsCategory pmsCategory) {
        pmsCategoryDao.insertTemplate(pmsCategory);
    }

//    @CacheEvict(value = Constant.CACHE_CATEGORY,key = "'getLevel1'")
    @Caching(evict = {
            @CacheEvict(value = Constant.CACHE_CATEGORY,key = "'getLevel1'"),
            @CacheEvict(value = Constant.CACHE_CATEGORY,key = "'getCatelogJson'")
    })
    @Override
    @Transactional
    public void updateMenu(PmsCategory pmsCategory) {
        pmsCategoryDao.updateTemplateById(pmsCategory);

        pmsCategoryBrandRelationService.updateCategory(pmsCategory.getCatId(),pmsCategory.getName());
    }

    @Override
    public PmsCategory getMenuOne(Integer catId) {
        return pmsCategoryDao.single(catId);
    }

    @Override
    public PmsCategory getById(Long catId) {
        return pmsCategoryDao.single(catId);
    }

    @Override
    public void updateMenuSort(List<PmsCategory> pmsCategories) {
        pmsCategoryDao.getSQLManager()
                .updateBatchTemplateById(PmsCategory.class,pmsCategories);
    }

    //[2,26,225]
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> list = CollUtil.newArrayList();
        PmsCategory category = pmsCategoryDao.single(catelogId);
        list.add(catelogId);
        if (!Constant.NOT_PARENT.equals(category.getParentCid())){
            PmsCategory c2 = pmsCategoryDao.single(category.getParentCid());
            if (ObjectUtil.isNotNull(c2)){
                list.add(c2.getCatId());
                if (!Constant.NOT_PARENT.equals(c2.getParentCid())){
                    PmsCategory c3 = pmsCategoryDao.single(c2.getParentCid());
                    if (ObjectUtil.isNotNull(c3)){
                        list.add(c3.getCatId());
                    }
                }
            }
        }
        CollUtil.reverse(list);
        return ArrayUtil.toArray(list,Long.class);
    }


    /**
     * @Cacheable: 当前方法的结果需要缓存 并指定缓存名字
     *  缓存的value值 默认使用jdk序列化
     *  默认ttl时间 -1
     *	key: 里面默认会解析表达式 字符串用 ''
     *
     *  自定义:
     *  	1.指定生成缓存使用的key
     *  	2.指定缓存数据存活时间	[配置文件中修改]
     *  	3.将数据保存为json格式
     *
     *  sync = true: --- 开启同步锁
     *
     */
    @Cacheable(value = Constant.CACHE_CATEGORY,key = "#root.method.name")
    @Override
    public List<PmsCategory> getLevel1() {
        List<PmsCategory> select = pmsCategoryDao.createLambdaQuery()
                .andEq(PmsCategory::getCatLevel, Constant.CAT_LEVEL1)
                .select();
        return select;
    }

    //TODO redis 产生堆外内存溢出OutOfDirectMemoryError
    @Cacheable(value = Constant.CACHE_CATEGORY,key = "#root.methodName" )
    @Override
    public String getCatelogJson() {

        /**
         * TODO 缓存
         * 1.空结果缓存：解决缓存穿透
         * 2.设置随机过期时间：解决缓存雪崩
         * 3.加锁：解决缓存击穿
         */

        //String catalog = stringRedisTemplate.opsForValue().get(Constant.CATALOG);

        return getCatelogJsonFromDb();
    }

    public String getCatelogJsonFromDb() {
        //本地锁
//        return localLock();

        //redis分布式锁
//        return redisLock();

        //redisson分布式锁
        return redissonLock();
    }

    /**
     * 本地锁
     * synchronized (this)：springBoot所有组件在容器中默认是单例的
     * @return
     */
    private String localLock(){
        synchronized (this) {
            return getCatelogFromDb();
        }
    }

    /**
     * redis分布式锁
     * 加锁保证原子行
     * 删除锁保证原子性
     * @return
     */
    private String redisLock(){
        String uuid = UUID.randomUUID().toString();
        //设置锁的同时，加上过期时间，防止死锁
        Boolean productLock = stringRedisTemplate.opsForValue().setIfAbsent("productLock", uuid,30L,TimeUnit.SECONDS);
        if (productLock){
            //加锁成功
            //删除锁 lua脚本解锁

            // 删除也必须是原子操作 Lua脚本操作 删除成功返回1 否则返回0
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            // 原子删锁
            stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("productLock"), uuid);
            return getCatelogFromDb();
        }
        else {
            // 重试加锁
            try {
                // 登上两百毫秒
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return redisLock();
        }
    }

    /**
     * redisson分布式锁
     * 缓存里面的数据如何和数据库保持一致
     * 缓存数据一致性
     * 1）、双写模式：更新数据库数据，把缓存数据重写
     * 2）、失效模式：更新数据库数据，把缓存数据查询，等待下一次去数据库查询
     * @return
     */
    private String redissonLock(){

        //占分布式锁
        RLock lock = redissonClient.getLock(Constant.CATALOGJSON_LOCK);
        lock.lock();
        String data;
        try {
            data = getCatelogFromDb();
        } finally {
            lock.unlock();
        }
        return data;

    }

    private String getCatelogFromDb() {
        String catalog = stringRedisTemplate.opsForValue().get("getCatelogJson");
        if (StrUtil.isNotEmpty(catalog)) {
            System.out.println("分布式redis");
            return catalog;
        }
        System.out.println("查询了数据库");
        List<PmsCategory> all = pmsCategoryDao.all();
        //查出所有1级分类
        List<PmsCategory> level = getCategoryByPId(all, Constant.CAT_PARENT_CID);

        Map<String, List<Catelog2Vo>> map = level.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<PmsCategory> level2 = getCategoryByPId(all, v.getCatId());

            List<Catelog2Vo> catelog2Vos = null;
            if (CollUtil.isNotEmpty(level2)) {
                catelog2Vos = level2.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo();
                    catelog2Vo.setCatalog1Id(v.getCatId().toString());
                    catelog2Vo.setName(l2.getName());
                    catelog2Vo.setId(l2.getCatId().toString());
                    System.out.println("菜单2ID：" + l2.getCatId()+"==>" + level2);
                    List<PmsCategory> level3 = getCategoryByPId(all, l2.getCatId());
                    System.out.println("菜单3："  + level3);

                    List<Catalog3Vo> catalog3Vos = null;
                    if (CollUtil.isNotEmpty(level3)) {
                        catalog3Vos = level3.stream().map(l3 -> {
                            Catalog3Vo catalog3Vo = new Catalog3Vo();
                            catalog3Vo.setCatalog2Id(l2.getCatId().toString());
                            catalog3Vo.setId(l3.getCatId().toString());
                            catalog3Vo.setName(l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());

                        catelog2Vo.setCatalog3List(catalog3Vos);
                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        String jsonStr = JSONUtil.toJsonStr(map);
        //stringRedisTemplate.opsForValue().set(Constant.CATALOG, jsonStr, 1, TimeUnit.DAYS);
        return jsonStr;
    }

    private List<PmsCategory> getCategoryByPId(List<PmsCategory> list, Long parentId) {
        return list.stream()
                .filter(pmsCategory -> parentId.equals(pmsCategory.getParentCid()))
                .collect(Collectors.toList());
    }

}
